//-----------------------------------------------------------------------------
// Filename: Crypto.cs
//
// Description: Encrypts and decrypts data.
//
// History:
// 16 Jul 2005	Aaron Clauson	Created.
//
// License:
// Aaron Clauson
//-----------------------------------------------------------------------------

using System;
using System.IO;
using System.Net;
using System.Text;
using System.Security.Cryptography;
using log4net;

#if UNITTEST
using NUnit.Framework;
#endif

namespace BlueFace.Sys
{
	public class Crypto
	{
        public const int DEFAULT_RANDOM_LENGTH = 10;    // Number of digits to return for default random numbers.
        
        private static ILog logger = AppState.logger;
		
		private static string m_randomNumberURL = AppState.RandomNumberURL;

		private static Random m_randomNumber;

		public static string RSAEncrypt(string xmlKey, string plainText)
		{
			return Convert.ToBase64String(RSAEncryptRaw(xmlKey, plainText));
		}

		public static byte[] RSAEncryptRaw(string xmlKey, string plainText)
		{
			try
			{
				RSACryptoServiceProvider key = GetKey(xmlKey);
				
				return key.Encrypt(Encoding.ASCII.GetBytes(plainText), true);
			}
			catch(Exception excp)
			{
				logger.Error("Exception RSAEncrypt. " + excp.Message);
				throw excp;
			}
		}
	
		public static string RSADecrypt(string xmlKey, string cypherText)
		{
			return Encoding.ASCII.GetString(RSADecryptRaw(xmlKey, Convert.FromBase64String((cypherText))));
		}
	
		public static byte[] RSADecryptRaw(string xmlKey, byte[] cypher)
		{
			try
			{
				RSACryptoServiceProvider key = GetKey(xmlKey);		

				return key.Decrypt(cypher, true);
			}
			catch(Exception excp)
			{
				logger.Error("Exception RSADecrypt. " + excp.Message);
				throw excp;
			}
		}
	
		private static RSACryptoServiceProvider GetKey(string xmlKey)
		{
			CspParameters cspParam = new CspParameters();
			cspParam.Flags = CspProviderFlags.UseMachineKeyStore;
			RSACryptoServiceProvider key = new RSACryptoServiceProvider(cspParam);
			key.FromXmlString(xmlKey);

			return key;
		}

		public static string GetRandomString()
		{
            return GetRandomInt(DEFAULT_RANDOM_LENGTH).ToString();
		}

        /// <summary>
		/// Returns a 10 digit random number.
		/// </summary>
		/// <returns></returns>
        public static int GetRandomInt()
        {
            return GetRandomInt(DEFAULT_RANDOM_LENGTH);
        }

        /// <summary>
        /// Returns a random number of a specified length.
        /// </summary>
        public static int GetRandomInt(int length)
        {
            int randomStart = 1000000000;
            int randomEnd = Int32.MaxValue;

            if (length > 0 && length < DEFAULT_RANDOM_LENGTH)
            {
                randomStart = Convert.ToInt32(Math.Pow(10, length - 1));
                randomEnd = Convert.ToInt32(Math.Pow(10, length) - 1);
            }

            return GetRandomInt(randomStart, randomEnd);
        }

        /// <summary>
        /// Returns a random number between start and end.
        /// </summary>
		public static int GetRandomInt(int start, int end)
		{
            if (m_randomNumber == null)
            {
                lock (typeof(Crypto))
                {

                    int randomSeed = 0;

                    try
                    {
                        logger.Debug("Attempting to obtain random seed from random.org, timeout is 3s.");

                        // Attempt a call to the web service from random.org to get intial random seed.
                        HttpWebRequest req = (HttpWebRequest)WebRequest.Create(m_randomNumberURL);
                        req.Timeout = 3000;

                        StreamReader stIn = new StreamReader(req.GetResponse().GetResponseStream());
                        string strResponse = stIn.ReadToEnd();

                        //logger.Debug("Response from " + m_randomNumberURL + " was " + strResponse);

                        randomSeed = Convert.ToInt32(strResponse);

                        stIn.Close();

                        logger.Debug("Random seed retrieved from random.org = " + randomSeed + ".");
                    }
                    catch
                    {
                        //Console.WriteLine("Exception " + excp.Message);

                        // If the web service times out use the system clock.
                        long longRandomSeed = DateTime.Now.Ticks;

                        randomSeed = Convert.ToInt32(longRandomSeed % Int32.MaxValue);

                        logger.Debug("Random seed retrieval from random.org timed out, seed retrieved from system clock time = " + randomSeed + ".");
                        //Console.WriteLine("Random seed retrieved from system clock time = " + longRandomSeed + ".");
                    }

                    //logger.Debug("Random seed = " + randomSeed);

                    m_randomNumber = new Random(randomSeed);
                }
            }

			return m_randomNumber.Next(start, end);
		}

		public static string GetRandomString(int length)
		{
			string randomStr = GetRandomInt().ToString();

			return (randomStr.Length > length) ? randomStr.Substring(0, length) : randomStr;
		}

		public static byte[] createRandomSalt(int Length)
		{
			// Create a buffer
			byte[] randBytes;

			if (Length >= 1)
			{
				randBytes = new byte[Length];
			}
			else
			{
				randBytes = new byte[1];
			}

			// Create a new RNGCryptoServiceProvider.
			RNGCryptoServiceProvider rand = new RNGCryptoServiceProvider();

			// Fill the buffer with random bytes.
			rand.GetBytes(randBytes);

			// return the bytes.
			return randBytes;
		}
		
		public static void clearBytes(byte[] Buffer)
		{
			// Check arguments.
			if (Buffer == null)
			{
				throw new ArgumentException("Buffer");
			}

			// Set each byte in the buffer to 0.
			for (int x = 0; x <= Buffer.Length - 1; x++)
			{
				Buffer[x] = 0;
			}
		}
		
		public static string GetSHAHash(string plainText)
		{
			SHA1Managed shaHash = new SHA1Managed();
			
			return Convert.ToBase64String(shaHash.ComputeHash(Encoding.ASCII.GetBytes(plainText)));
		}

        public static string GetSHAHashAsHex(string plainText)
        {
            SHA1Managed shaHash = new SHA1Managed();
            byte[] hash = shaHash.ComputeHash(Encoding.ASCII.GetBytes(plainText));
            StringBuilder hashBuilder = new StringBuilder();
            for (int index = 0; index < hash.Length; index++)
            {
                hashBuilder.Append(hash[index].ToString("X"));
            }

            return hashBuilder.ToString();
        }

        /// <summary>
        /// This vesion reads the whole file in at once. This is not great since it can consume
        /// a lot of memory if the file is large. However a buffered approach generates
        /// diferrent hashes across different platforms.
        /// </summary>
        /// <param name="filepath"></param>
        /// <returns></returns>
        public static string GetHash(string filepath)
		{
			// Check and then attempt to open the plaintext stream.
			FileStream fileStream = GetFileStream(filepath);
			
			// Encrypt the file using its hash as the key.
			SHA1 shaM = new SHA1Managed();

			// Buffer to read in plain text blocks.
			byte[] fileBuffer = new byte[fileStream.Length];
			fileStream.Read(fileBuffer, 0, (int)fileStream.Length);
			fileStream.Close();

			byte[] overallHash = shaM.ComputeHash(fileBuffer);

			return Convert.ToBase64String(overallHash);
		}

		/// <summary>
		/// Used by methods wishing to perform a hash operation on a file. This method
		/// will perform a number of checks and if happy return a read only file stream.
		/// </summary>
		/// <param name="filepath">The path to the input file for the hash operation.</param>
		/// <returns>A read only file stream for the file or throws an exception if there is a problem.</returns>
		private static FileStream GetFileStream(string filepath)
		{
			// Check that the file exists.
			if(!File.Exists(filepath))
			{
				logger.Error("Cannot open a non-existent file for a hash operation, " + filepath + ".");
				throw new IOException("Cannot open a non-existent file for a hash operation, " + filepath + ".");
			}

			// Open the file.
			FileStream inputStream = File.OpenRead(filepath);

			if(inputStream.Length == 0)
			{
				inputStream.Close();
				logger.Error("Cannot perform a hash operation on an empty file, " + filepath + ".");
				throw new IOException("Cannot perform a hash operation on an empty file, " + filepath + ".");
			}

			return inputStream;
		}

		#region Unit testing.

		#if UNITTEST
	
		[TestFixture]
		public class CryptoUnitTest
		{
			[TestFixtureSetUp]
			public void Init()
			{
				
			}

			[TestFixtureTearDown]
			public void Dispose()
			{			
				
			}

			[Test]
			public void SampleTest()
			{
				Console.WriteLine("--> " + System.Reflection.MethodBase.GetCurrentMethod().Name);
				
				m_randomNumber = null;

				int initRandomNumber = Crypto.GetRandomInt();
				
				Console.WriteLine("Random int = " + initRandomNumber + ".");
				
				Console.WriteLine("-----------------------------------------");
			}

			[Test]
			public void CallRandomNumberWebServiceUnitTest()
			{
				Console.WriteLine("--> " + System.Reflection.MethodBase.GetCurrentMethod().Name);

				Console.WriteLine("Random number = " + GetRandomInt());

				Console.WriteLine("-----------------------------------------");
			}

			[Test]
			public void GetRandomNumberTest()
			{
				Console.WriteLine("--> " + System.Reflection.MethodBase.GetCurrentMethod().Name);

				Console.WriteLine("Random number = " + GetRandomInt());

				Console.WriteLine("-----------------------------------------");
			}

			[Test]
			public void GetOneHundredRandomNumbersTest()
			{
				Console.WriteLine("--> " + System.Reflection.MethodBase.GetCurrentMethod().Name);

				for(int index=0; index<100; index++)
				{
					Console.WriteLine("Random number = " + GetRandomInt());
				}

				Console.WriteLine("-----------------------------------------");
			}
		}

		#endif

		#endregion
	}
}
